using System;
using System.IO;
using System.Text;
using ParserGenerator.Extensions;

namespace ParserGenerator.Input
{
    [Serializable]
    public sealed class SourceReader : IDisposable
    {
        private const int DefaultTabSize = 8;

        private readonly SourceLocation _begin;
        private readonly TextReader _stream;
        private readonly bool _takeOwn;
        private readonly bool _stdin;

        private string _firstPromptString;
        private string _restPromptString;
        private int _tabSize = DefaultTabSize;
        private int _endIndex;
        private SourceLocation _location;
        private StringBuilder _sb;
        private char[] _buffer;
        private bool _eof;

        public SourceDocument Document => _begin.Document;
        public SourceLocation Begin => _begin;
        public bool IsInteractive => _stdin && ConsoleExtensions.ConsoleIsInteractive;

        public string FirstPromptString
        {
            get { return _firstPromptString; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException(nameof(value));
                _firstPromptString = value;
            }
        }

        public string RestPromptString
        {
            get { return _restPromptString; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException(nameof(value));
                _restPromptString = value;
            }
        }

        public int TabSize
        {
            get { return _tabSize; }
            set
            {
                if (value <= 0)
                    throw new ArgumentOutOfRangeException(nameof(value), SR.TabSizeMustBePositive);
                _tabSize = value;
            }
        }

        public SourceLocation Location
        {
            get { return _location; }
            set
            {
                if (value.Document != _begin.Document)
                    throw new ArgumentException(SR.LocationComesFromDiffDoc, nameof(value));
                if (value.Index < _begin.Index || value.Index > _endIndex)
                    throw new ArgumentOutOfRangeException(nameof(value), SR.LocationOutOfRange);
                _location = value;
            }
        }

        private SourceReader(SourceLocation begin, int endIndex, TextReader stream, bool takeOwn, bool stdin)
        {
            _begin = begin;
            _endIndex = endIndex;
            _stream = stream;
            _takeOwn = takeOwn;
            _stdin = stdin;
            _location = begin;
            _firstPromptString = ">>> ";
            _restPromptString = "... ";
        }

        public static SourceReader FromString(string source)
        {
            if (source == null)
                throw new ArgumentNullException(nameof(source));
            return FromString("<string>", source);
        }

        public static SourceReader FromString(string name, string source)
        {
            if (name == null)
                throw new ArgumentNullException(nameof(name));
            if (source == null)
                throw new ArgumentNullException(nameof(source));
            return FromString(name, name, source);
        }

        public static SourceReader FromString(string name, string fullName, string source)
        {
            if (name == null)
                throw new ArgumentNullException(nameof(name));
            if (fullName == null)
                throw new ArgumentNullException(nameof(fullName));
            if (source == null)
                throw new ArgumentNullException(nameof(source));
            SourceDocument document = new StringDocument(name, fullName, source);
            SourceLocation begin = new SourceLocation(document, 0, 0, 0, 0);
            int endIndex = source.Length;
            return new SourceReader(begin, endIndex, null, false, false);
        }

        public static SourceReader FromContext(SourceContext context)
        {
            if (context == null)
                throw new ArgumentNullException(nameof(context));
            return new SourceReader(context.Begin, context.End.Index, null, false, false);
        }

        public static SourceReader FromStream(string name, TextReader stream)
        {
            if (name == null)
                throw new ArgumentNullException(nameof(name));
            if (stream == null)
                throw new ArgumentNullException(nameof(stream));
            return FromStream(name, name, stream);
        }

        public static SourceReader FromStream(string name, string fullName, TextReader stream)
        {
            if (name == null)
                throw new ArgumentNullException(nameof(name));
            if (fullName == null)
                throw new ArgumentNullException(nameof(fullName));
            if (stream == null)
                throw new ArgumentNullException(nameof(stream));
            return FromStream(name, fullName, stream, false);
        }

        public static SourceReader FromStream(string name, TextReader stream, bool takeOwn)
        {
            if (name == null)
                throw new ArgumentNullException(nameof(name));
            if (stream == null)
                throw new ArgumentNullException(nameof(stream));
            return FromStream(name, name, stream, takeOwn);
        }

        public static SourceReader FromStream(string name, string fullName, TextReader stream, bool takeOwn)
        {
            if (name == null)
                throw new ArgumentNullException(nameof(name));
            if (fullName == null)
                throw new ArgumentNullException(nameof(fullName));
            if (stream == null)
                throw new ArgumentNullException(nameof(stream));
            SourceDocument document = new StreamDocument(name, fullName);
            SourceLocation begin = new SourceLocation(document, 0, 0, 0, 0);
            return new SourceReader(begin, 0, stream, takeOwn, false);
        }

        public static SourceReader FromConsole()
        {
            const string name = "<stdin>";
            SourceDocument document = new StreamDocument(name, name);
            SourceLocation begin = new SourceLocation(document, 0, 0, 0, 0);
            return new SourceReader(begin, 0, null, false, true);
        }

        public void Dispose()
        {
            if (_stream != null && _takeOwn)
                _stream.Dispose();
        }

        public void Rewind()
        {
            _location = _begin;
        }

        private void UpdateSource()
        {
            string source = _sb.ToString();
            StreamDocument doc = Document as StreamDocument;
            doc.UpdateSource(source);
            _endIndex = source.Length;
        }

        public int Peek(int offset)
        {
            int i = Location.Index + offset;
            while (true)
            {
                if (_begin.Index <= i && i < _endIndex)
                    return Document.GetSource()[i];
                if (_stream == null && !_stdin)
                    return -1;
                if (_stream != null)
                {
                    if (_eof)
                        return -1;
                    if (_buffer == null)
                        _buffer = new char[4096];
                    int count = _stream.Read(_buffer, 0, _buffer.Length);
                    if (count == 0)
                    {
                        if (_sb == null)
                            _sb = new StringBuilder();
                        if (_sb.Length > 0 && !_sb[_sb.Length - 1].IsLineTerminator())
                        {
                            _sb.Append("\n");
                            UpdateSource();
                        }
                        else
                        {
                            _eof = true;
                            return -1;
                        }
                    }
                    else
                    {
                        if (_sb == null)
                            _sb = new StringBuilder();
                        _sb.Append(_buffer, 0, count);
                        UpdateSource();
                    }
                }
                if (_stdin)
                {
                    if (_eof)
                        return -1;
                    string line;
                    if (IsInteractive)
                    {
                        string ps = (Location.Line == 0) ? _firstPromptString : _restPromptString;
                        using (ConsoleExtensions.PromptColor.AsForegroundColor())
                            Console.Error.Write(ps);
                        using (ConsoleExtensions.InputColor.AsForegroundColor())
                            line = Console.In.ReadLine();
                    }
                    else
                    {
                        line = Console.In.ReadLine();
                    }
                    if (line == null)
                    {
                        _eof = true;
                        return -1;
                    }
                    else
                    {
                        if (_sb == null)
                            _sb = new StringBuilder();
                        _sb.Append(line);
                        _sb.Append("\n");
                        UpdateSource();
                    }
                }
            }
        }

        public int Peek()
        {
            return Peek(0);
        }

        public int Read()
        {
            int c = Peek();
            if (c != -1)
            {
                int index = Location.Index + 1;
                int line = Location.Line;
                int offset = Location.Offset;
                int column = Location.Column;
                if (c.IsLineTerminator())
                {
                    if (c == '\r' && Peek(1) == '\n')
                    {
                        c = '\n';
                        index++;
                    }
                    line++;
                    offset = 0;
                    column = 0;
                }
                else
                {
                    offset++;
                    if (c == '\t')
                        column = (column / _tabSize + 1) * _tabSize;
                    else
                        column++;
                }
                _location = new SourceLocation(Document, index, line, offset, column);
            }
            return c;
        }

        [Serializable]
        private sealed class StringDocument : SourceDocument
        {
            private readonly string _name;
            private readonly string _fullName;
            private readonly string _source;

            public sealed override string Name => _name;
            public sealed override string FullName => _fullName;
            public sealed override string TryGetSource() => _source;
            public sealed override string GetSource() => _source;

            public StringDocument(string name, string fullName, string source)
            {
                if (name == null)
                    throw new ArgumentNullException(nameof(name));
                if (fullName == null)
                    throw new ArgumentNullException(nameof(fullName));
                if (source == null)
                    throw new ArgumentNullException(nameof(source));
                _name = name;
                _fullName = fullName;
                _source = source;
            }
        }

        [Serializable]
        private sealed class StreamDocument : SourceDocument
        {
            private readonly string _name;
            private readonly string _fullName;
            private string _source;

            public sealed override string Name => _name;
            public sealed override string FullName => _fullName;
            public sealed override string TryGetSource() => _source;
            public sealed override string GetSource() => _source;

            public StreamDocument(string name, string fullName)
            {
                if (name == null)
                    throw new ArgumentNullException(nameof(name));
                if (fullName == null)
                    throw new ArgumentNullException(nameof(fullName));
                _name = name;
                _fullName = fullName;
            }

            public void UpdateSource(string source)
            {
                if (source == null)
                    throw new ArgumentNullException(nameof(source));
                _source = source;
            }
        }
    }
}
